Skip to content

Conversation

@dkhalanskyjb
Copy link
Collaborator

Fixes #4516

Background

Whenever an uncaught exception happens in a coroutine, it gets reported to the CoroutineExceptionHandler. See https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler. However, if it's not installed, a platform-specific handler is used.
On the JVM, this means invoking the thread's
UncaughtExceptionHandler, which logs the exception to the console by default, but can be configured to do other things (for example, on Android, it will crash the application).

Problem

User-specified UncaughtExceptionHandler instances are allowed to throw exceptions.
Java's documentation says so
(https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.UncaughtExceptionHandler.html):

Any exception thrown by this method will be ignored by the Java Virtual Machine.

This means a user is allowed to write a throwing
UncaughtExceptionHandler, and the caller has to deal with it.

In our implementation, however, we are simply invoking the exception handler as a plain function, and if that function throws an exception, we allow this exception to propagate to the coroutine machinery, causing it to fail.

Solution

To comply with the contract defined for UncaughtExceptionHandler, we also ignore the exceptions thrown from there.

Fixes #4516

Background
----------

Whenever an *uncaught* exception happens in a coroutine,
it gets reported to the `CoroutineExceptionHandler`.
See <https://kotlinlang.org/docs/exception-handling.html#coroutineexceptionhandler>.
However, if it's not installed, a platform-specific handler
is used.
On the JVM, this means invoking the thread's
`UncaughtExceptionHandler`, which logs the exception to the console
by default, but can be configured to do other things
(for example, on Android, it will crash the application).

Problem
-------

User-specified `UncaughtExceptionHandler` instances are allowed to
throw exceptions.
Java's documentation says so
(https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.UncaughtExceptionHandler.html):
> Any exception thrown by this method will be ignored by the Java Virtual Machine.

This means a user is allowed to write a throwing
`UncaughtExceptionHandler`, and the caller has to deal with it.

In our implementation, however, we are simply invoking the
exception handler as a plain function, and if that function
throws an exception, we allow this exception to propagate
to the coroutine machinery, causing it to fail.

Solution
--------

To comply with the contract defined for `UncaughtExceptionHandler`,
we also ignore the exceptions thrown from there.
* > Any exception thrown by this method will be ignored by the Java Virtual Machine.
*
* This means the authors of the thread exception handlers have the right to throw exceptions indiscriminately.
* We have no further channels for propagating the fatal exception, so we give up. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can add something like val _ = "Welcome, weary traveler. To my breakpoint place" which is both self-explanatory and funny (okay, maybe only self-explanatory)

try {
exceptionHandler.uncaughtException(currentThread, exception)
} catch (_: Throwable) {
/* Do nothing.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we also add a sample?

val handler = CoroutineExceptionHandler { _, e ->
    // e is IllegalStateException("Will be handled")
    throw IllegalStateException("Will be thrown and ignored")
}
GlobalScope.launch(handler) {
    throw IllegalStateException("Will be handled")
}.join()
println("This sample finishes successfully, without any exceptions.")

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test should serve as a sort of sample that is more precisely describing what happens and won't get outdated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we like to know if we change this behaviour?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we would, which the test accommodates, but a textual sample does not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in: the sample could be a test (and there's an effort to make it automatically a test)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the behavior that testThrowingUncaughtExceptionHandler checks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree but I exhausted the discussion budget on this. Let it be your way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you, but if you're unclear on why testThrowingUncaughtExceptionHandler tests this exact behavior, please let me know.

}) {
launch(Job()) {
expect(2)
throw TestException("to be reported")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Will be handled"

expect(1)
val caughtException = catchingUncaughtException {
GlobalScope.launch(CoroutineName("last-ditch")) {
GlobalScope.launch(CoroutineName("last-resort")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I realised that CoroutineName("last-resort") should be just CoroutineName("some contextual information")

@dkhalanskyjb dkhalanskyjb merged commit d209433 into develop Nov 5, 2025
1 check passed
@dkhalanskyjb dkhalanskyjb deleted the dkhalanskyjb/ignore-exceptionhandler-crashes branch November 5, 2025 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants